(本篇文章網誌版:http://shineright.blogspot.tw/2016/12/day-23.html)
昨天做完結束畫面後,這個遊戲大致完成了。
但遊戲進行時,背景一片藍藍的,實在很醜。所以今天就來把背景變得動態、好看一點。
先說一下我想做的背景長什麼樣子。我希望背景可以分為早上和晚上,早上背景呈淺藍色,並有一顆太陽從螢幕右側緩緩移向左側,晚上背景則呈深藍色,太陽則變成月亮。此外,早上會不斷有雲從上往下飄,讓玩家有主角在向上長高的感覺。晚上則把飄動的雲改成星星。
在Scene中建立一個新的Sprite,命名為「Background」,並把它的Sprite指定為一個全白的矩形,這就是隨著早上和晚上改變顏色的背景了。把它的Transfrom Z變成正數(如5),使它在所有Game Object的最後方。
建立一個名為「BackgroundAnimation.cs」的C# Script。我一步一步來完成要呈現的動畫。先來做緩緩由淺藍變成深藍,再緩緩變回淺藍的背景。
public class BackgroundAnimation : MonoBehaviour
{
//白天或晚上的持續時間
public float dayNightDuration;
//白天時背景的顏色(淺藍色)
public Color dayColor;
//晚上時背景的顏色(深藍色)
public Color nightColor;
void Start()
{
StartCoroutine (MainCoroutine ());
}
//控制白天<->晚上的主要Coroutine
IEnumerator MainCoroutine()
{
while (true) {
//晚上,從nightColor變到dayColor
StartCoroutine (ColorLerpCoroutine (nightColor, dayColor));
//過了dayNightDuration時間,變成早上
yield return new WaitForSeconds (dayNightDuration);
//白天,從dayColor變到nightColor
StartCoroutine (ColorLerpCoroutine (dayColor, nightColor));
//過了dayNightDuration時間,變成晚上
yield return new WaitForSeconds (dayNightDuration);
}
}
//控制背景顏色的Coroutine
IEnumerator ColorLerpCoroutine(Color fromColor, Color toColor)
{
float i = 0f;
while (i <= 1f) {
//計算上影格到這影格的時間佔dayNightDuration多少時間,並加至i上
i += Time.deltaTime / dayNightDuration;
//設定SpriteRenderer的color
GetComponent<SpriteRenderer> ().color = Color.Lerp (fromColor, toColor, i);
yield return null;
}
}
}
這段程式碼有許多複雜的地方。首先來講public
變數,dayNightDuration
代表白天和晚上的持續時間,也就是由白天轉換到晚上、晚上轉換到白天所需的時間。同為Color
型態的dayColor
和nightColor
則可讓我在Inspector中選則白天和晚上背景的顏色。
Start()
中,簡單地使MainCoroutine()
開始。
MainCoroutine()
是這段程式碼中最主要的Coroutine,用以控制白天和晚上的變化。我讓遊戲開始於晚上,並使ColorLerpCoroutine(nightColor, dayColor)
開始,讓背景的顏色由nightColor
緩緩轉變為dayColor
。呼叫ColorLerpCoroutine()
緩緩更變顏色時,MainCoroutine()
以yield return new WaitForSeconds(dayNightDuration)
等待顏色更變完成,邁入早上。
最後,來看ColorLerpCoroutine()
。這其實是一個Unity Scripting中非常典型的Coroutine。它會在一段時間內緩緩改變Game Object的某一屬性。我先設float i=0f
,進入while
後,i會不斷隨時間增加,增加的量為「過去的時間」和「更變屬性動畫的總時間」的比例,也就是Time.deltaTime / dayNightDuration
。如此,在一次次的while
中,i
會不斷隨時間由0增加到1。但這個i
究竟是要幹嘛的呢?其實是為了底下呼叫線性插值法(Linear Interpolation,也就是Lerp)用的。Color.Lerp(fromColor, toColor, i)
是使用插值法(fromColor + i x (toColor - fromColor))
回傳一個fromColor
到toColor
之間的數。更改完顏色,以yield return null
把控制交還給Unity,等待下一個影格再次運行while()
。
回到Unity Editor,為背景Sprite加上Background Animation (Script) Component,設定好早上晚上的持續時間與顏色,進入Play Mode,背景顏色就會慢慢變換了。
接著來做移動的月亮與太陽。為BackgroundAnimation
加上幾行public
變數:
public Transform sunMoonStartTransform; //太陽/月亮起始位置
public Transform sunMoonEndTransform; //太陽/月亮最終位置
public GameObject sun; //太陽
public GameObject moon; //月亮
再多寫一個Coroutine:
IEnumerator SunMoonMovingCoroutine(GameObject obj)
{
float i = 0f;
while (i <= 1f) {
i += Time.deltaTime / dayNightDuration;
obj.transform.position = Vector3.Lerp (sunMoonStartTransform.transform.position, sunMoonEndTransform.transform.position, i);
yield return null;
}
}
這個Coroutine和改變背景顏色的Coroutine很像,只是把Color
換成Vector3
。這會讓月亮或太陽隨時間從sunMoonStartTransform
的位置移動到sunMoonEndTransform
的位置。
寫完新的Coroutine就可以來為MainCoroutine
加料了。在MainCoroutine
的//晚上
和//白天
處各加上
StartCoroutine (SunMoonMovingCoroutine (moon));
和
StartCoroutine (SunMoonMovingCoroutine (sun));
回到Unity Editor。新增兩個Game Object,分別代表太陽/月亮的起點和終點,並增加太陽和月亮的Game Object到Scene上,再把它們拉至Background Animation (Script) Component對應的欄位。進入Play Mode,現在不只背景顏色變化,日月也會交替了。
最後,來做下降的星星和雲吧!把不同形狀的星星和雲的Sprite都拉到Scene上,為它們增加Random Initial Speed (Script)、Destroy After Seconds (Script),並設定好數值(Destroy After Seconds應設為從螢幕最上方到螢幕最下方所需的時間)。把它們都拉到Project欄成為Prefab,就可以再度開啟BackgroundAnimation.cs,為背景動畫加工了。
在BackgroundAnimation.cs再新增幾個public
變數。
public Transform cloudStarMinSpawnTransform; //星星或雲生成的最小(左)位置
public Transform cloudStarMaxSpawnTransform; //星星或雲生成的最大(右)位置
public GameObject[] cloudPrefabs; //所有雲的Prefab
public GameObject[] starPrefabs; //所有星星的Prefab
public float cloudStarSpawnMinInterval; //一波星星或雲的最小間隔時間
public float cloudStarSpawnMaxInterval; //一波星星或雲的最大間隔時間
public int cloudStarSpawnMinAmount; //一波星星或雲的最小個數
public int cloudStarSpawnMaxAmount; //一波星星或雲的最大個數
接著,再新增一個Coroutine:
IEnumerator SpawnCloudStarCoroutine(bool cloud)
{
while (true) {
//要產生星星或雲的數量
int spawnAmount = Random.Range (cloudStarSpawnMinAmount, cloudStarSpawnMaxAmount + 1);
while (spawnAmount > 0) {
//產生
Instantiate(
cloud ? cloudPrefabs[Random.Range(0, cloudPrefabs.Length)] : starPrefabs[Random.Range(0, starPrefabs.Length)],
new Vector3(
Random.Range(cloudStarMinSpawnTransform.transform.position.x, cloudStarMaxSpawnTransform.transform.position.x),
cloudStarMinSpawnTransform.transform.position.y,
cloudStarMinSpawnTransform.transform.position.z
),
Quaternion.identity
);
spawnAmount--;
}
//等待下一波的時間
yield return new WaitForSeconds (Random.Range (cloudStarSpawnMinInterval, cloudStarSpawnMaxInterval));
}
}
這個Coroutine相當簡單,每隔一段時間產生一波星星或雲,而每波星星或雲的數量都不一樣(在[cloudStarSpawnMinAmount
, cloudStarSpawnMaxAmount
]之間)。每個產生的星星或雲都在cloudStarMaxSpawnTransform
和cloudStarMinSpawnTransform
之間。因為這兩個座標只有X軸相異,所以也只有X軸需呼叫Random.Range()
產生隨機數值。
完成新的Coroutine後,馬上來修變MainCoroutine()
。
IEnumerator MainCoroutine()
{
//儲存上一個生成星星或雲的Coroutine
Coroutine spawnCoroutine = null;
while (true) {
//若上一個生成雲的Coroutine存在,則停止它
if (spawnCoroutine != null)
StopCoroutine (spawnCoroutine);
//呼叫生成星星Coroutine
spawnCoroutine = StartCoroutine (SpawnCloudStarCoroutine (false));
//—-↓舊的程式碼↓—-//
StartCoroutine (ColorLerpCoroutine (nightColor, dayColor));
StartCoroutine (SunMoonMovingCoroutine (moon));
yield return new WaitForSeconds (dayNightDuration);
//—-↑舊的程式碼↑—-//
//停止生成星星的Coroutine
StopCoroutine (spawnCoroutine);
//呼叫生成雲的Coroutine
spawnCoroutine = StartCoroutine (SpawnCloudStarCoroutine (true));
//—-↓舊的程式碼↓—-//
StartCoroutine (ColorLerpCoroutine (dayColor, nightColor));
StartCoroutine (SunMoonMovingCoroutine (sun));
yield return new WaitForSeconds (dayNightDuration);
//—-↑舊的程式碼↑—-//
}
}
沒錯,Coroutine可以像變數一樣被傳來傳數。因為在呼叫下一個SpawnCloudStarCoroutine
之前,必須先把前一個停止(生成雲之前需把生成星星的Coroutine停掉,反之亦然)。所以必須另宣告一個Coroutine形態的變數來儲存上一個Coroutine,之後才能以StopCoroutine()
停止它。此外,一開始因為spawnCoroutine
是null
,所以必須多一個if
式來檢查,不然會停止到不存在的Coroutine。
回到Unity Editor,再新增兩個代表星星和雲生成範圍的Game Object,拖到Cloud Star Min/Max Spawn欄,並設定好它們生成的數值。進入Play Mode,星星和雲出現啦!
此外,要讓背景的雲和星星更多樣化,可以為它們的Prefab再加上下面兩段Script來隨機它們的旋轉角度和大小比例。
//隨機大小比例
public class RandomInitialLocalScale : MonoBehaviour
{
public float minVal;
public float maxVal;
void Start()
{
transform.localScale = transform.localScale * Random.Range (minVal, maxVal);
}
}
//隨機旋轉角度
public class RandomInitialRotation : MonoBehaviour
{
void Start()
{
transform.rotation = Quaternion.Euler (0f, 0f, Random.Range (0f, 359f));
}
}
待續。